package au.edu.educationau.opensource.dsm.adapters.contenthandler;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang.text.StrSubstitutor;
import org.apache.log4j.Logger;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import au.edu.educationau.opensource.dsm.adapters.SearchAdapterProperties;
import au.edu.educationau.opensource.dsm.obj.ResultCategory;
import au.edu.educationau.opensource.dsm.obj.ResultMetadata;
import au.edu.educationau.opensource.dsm.obj.SearchCriteria;
import au.edu.educationau.opensource.dsm.obj.SearchResult;
import au.edu.educationau.opensource.dsm.service.PropertiesService;
import au.edu.educationau.opensource.dsm.worker.ResultSetUnifier;
import au.edu.educationau.opensource.dsm.worker.VocabMapper;

/**
 * Handler for SOLR data.
 * 
 * Crosswalk from SOLR to DSM fields is specified by config. eg:
 * 
 * <pre>
 * aanrores.metadata.crosswalk.date=startdate
 * aanrores.metadata.crosswalk.date.format=yyyyMM
 * aanrores.metadata.crosswalk.description=objective
 * aanrores.metadata.crosswalk.description.multihandling=concat
 * aanrores.metadata.crosswalk.link.derivation=http://devel:63080/aanrores/select?q=id:${id}&amp;wt=xslt&amp;tr=first.xsl
 * aanrores.metadata.crosswalk.metadata.ctype=doctype
 * </pre>
 * 
 * Brief explanations of above config:
 * <ul>
 * <li>the incoming 'startdate' field is crosswalked to the dsm date field.
 * </li>
 * <li>date.format specifies the format of the incoming date. </li>
 * <li>description.multilhandling specifies that multiple values from the
 * incoming 'objective' field are concatenated into the dsm description field.
 * </li>
 * <li>link.derivation means that the dsm link field is derived by substituting
 * the incoming id value into this url. </li>
 * <li>the incoming 'doctype' value is crosswalked to a dsm metadata field
 * named ctype. </li>
 * </ul>
 */
public class SolrContentHandler extends DefaultHandler {
	static Logger logger = Logger.getLogger(SolrContentHandler.class.getName());

	private final int MAX_RESULTS = 200;

	private SearchResult result = new SearchResult();

	private ArrayList results = new ArrayList(MAX_RESULTS);

	private String websiteURL = "";

	private Map params = new HashMap();

	private String paramName = "";

	private StringBuffer paramData = new StringBuffer();

	private ArrayList metadata = new ArrayList();

	private ArrayList categories = new ArrayList();

	private List metadataPruneList = new ArrayList();

	private ResultMetadata resultMetadata = null;

	/*
	 * Holds first value for each field for a doc. This is built up for each doc
	 * in case the metadata crosswalk needs to derive any values.
	 */
	private Map docFieldValues = null;

	private String fieldName = "";

	private StringBuffer fieldData = new StringBuffer();

	private Map facetQueryCounts = new HashMap();

	private String facetFieldName = "";

	private String facetQuery = "";

	private int facetQueryCount = 0;

	private SearchAdapterProperties props = new SearchAdapterProperties();

	/*
	 * A map keyed on SOLR field name whose value is the name of a DSM metadata
	 * element to map the value to.
	 */
	private Map solrToMetadataFieldMap = new HashMap();

	private SearchCriteria criteria = new SearchCriteria();

	// States
	private boolean readingResponse = false;

	private boolean readingResponseHeader = false;

	private boolean readingFacetQueries = false;

	private boolean readingFacetFields = false;

	private boolean readingFacetValues = false;

	private boolean readingParams = false;

	private boolean readingResult = false;

	private boolean readingField = false;

	private boolean readingParam = true;

	private boolean readingArray = false;

	private int found = 0;

	private int start = 0;

	/** Number of results returned in this response */
	public int getCount() {
		return results.size();
	}

	/** Number of results found on repository */
	public int getFound() {
		return found;
	}

	public int getStart() {
		return start;
	}

	/***/
	public ArrayList getResults() {
		ResultSetUnifier.normalizeResultsToZipfianDistribution(results, criteria);
		return results;
	}

	/**
	 * @param props
	 */
	public void setSearchAdapterProperties(SearchAdapterProperties props) {
		this.props = props;
		String metadataPruneListString = props.getExtraProperty(props.getAdapterCode() + ".metadata.prunelist");
		if (metadataPruneListString != null) {
			StringTokenizer st = new StringTokenizer(metadataPruneListString, ",");
			while (st.hasMoreTokens()) {
				metadataPruneList.add(st.nextToken().trim());
			}
		}

		// invert map so we can lookup by solr field name
		solrToMetadataFieldMap = MapUtils.invertMap(PropertiesService.getAllProperties(props.getAdapterCode() + ".metadata.crosswalk.metadata"));
	}

	/**
	 * 
	 * @param url
	 *            the websiteURL
	 */
	public void setWebsiteURL(String url) {
		websiteURL = url;
	}

	/**
	 * @param criteria
	 */
	public void setSearchCriteria(SearchCriteria criteria) {
		this.criteria = criteria;
	}

	/**
	 * @param ch
	 * @param ch_start
	 * @param ch_length
	 * @exception SAXException
	 */
	public void characters(char[] ch, int ch_start, int ch_length) throws SAXException {
		String val = String.copyValueOf(ch, ch_start, ch_length);
		if ("".equals(val.trim())) {
			return; // continue
		} else if (readingField) {
			fieldData.append(val);
		} else if (readingFacetQueries || readingFacetFields) {
			try {
				facetQueryCount = Integer.parseInt(val);
			} catch (Throwable t) {
				facetQueryCount = 0;
			}
		} else if (readingParam) {
			paramData.append(val);
		}
	}

	/**
	 * @param ch
	 * @param ch_start
	 * @param ch_length
	 * @exception SAXException
	 */
	public void ignorableWhitespace(char[] ch, int ch_start, int ch_length) throws SAXException {
	}

	/**
	 * @param uri
	 * @param localName
	 * @param qName
	 * @param attr
	 * @exception SAXException
	 */
	public void startElement(java.lang.String uri, java.lang.String localName, java.lang.String qName, Attributes attr) throws SAXException {
		if (localName.equals("response")) {
			// setup resultset
			readingResponse = true;
		} else if (localName.equals("lst") && attr.getValue("name") != null && attr.getValue("name").equals("responseHeader")) {
			// read header info
			readingResponseHeader = true;
			logger.trace("reading responseHeader");
		} else if (localName.equals("lst") && attr.getValue("name") != null && attr.getValue("name").equals("facet_queries")) {
			readingFacetQueries = true;
		} else if (localName.equals("lst") && attr.getValue("name") != null && attr.getValue("name").equals("facet_fields")) {
			readingFacetFields = true;
		} else if (localName.equals("lst") && readingFacetFields && attr.getValue("name") != null) {
			facetFieldName = attr.getValue("name");
			readingFacetValues = true;
		} else if (localName.equals("int") && readingFacetFields && attr.getValue("name") != null) {
			facetQuery = facetFieldName + ":" + attr.getValue("name");
			facetQueryCount = 0;
		} else if (localName.equals("int") && readingFacetQueries && attr.getValue("name") != null) {
			facetQuery = attr.getValue("name");
		} else if (readingResponseHeader && localName.equals("lst") && attr.getValue("name") != null && attr.getValue("name").equals("params")) {
			// read params
			readingParams = true;
		} else if (localName.equals("result") && attr.getValue("name") != null && attr.getValue("name").equals("response")) {
			// read number found and start
			params.put("found", attr.getValue("numFound"));
			params.put("start", attr.getValue("start"));
		} else if (localName.equals("doc")) {
			readingResult = true;
			docFieldValues = new HashMap();
		} else if (readingResult && attr.getValue("name") != null && (localName.equals("int") || localName.equals("str") || localName.equals("arr") || localName.equals("float") || localName.equals("long"))) {
			readingField = true;
			if (localName.equals("arr")) {
				readingArray = true;
			} else {
				readingArray = false;
			}
			fieldName = attr.getValue("name");
			fieldData = new StringBuffer();
		} else if (readingParams && attr.getValue("name") != null && (localName.equals("int") || localName.equals("str") || localName.equals("arr") || localName.equals("float") || localName.equals("long"))) {
			readingParam = true;
			paramName = attr.getValue("name");
			paramData = new StringBuffer();
		}
	}

	/**
	 * @param uri
	 * @param localName
	 * @param qName
	 * @exception SAXException
	 */
	public void endElement(java.lang.String uri, java.lang.String localName, java.lang.String qName) throws SAXException {
		if (localName.equals("response") && readingResponse) {
			// finalise result set
			readingResponse = false;
		} else if (localName.equals("lst") && readingParams) {
			readingParams = false;
		} else if (localName.equals("lst") && readingResponseHeader) {
			readingResponseHeader = false;
		} else if (localName.equals("lst") && readingFacetQueries) {
			readingFacetQueries = false;
		} else if (localName.equals("lst") && readingFacetValues) {
			readingFacetValues = false;
		} else if (localName.equals("lst") && readingFacetFields) {
			readingFacetFields = false;
		} else if (localName.equals("doc") && readingResult) {
			// finalise result
			readingResult = false;
			if (metadata.size() != 0) {
				result.setResultMetadata(getAsMetadataArray(metadata));
			}
			if (categories.size() != 0) {
				result.setResultCategories(getAsCategoryArray(categories));
			}
			metadata = new ArrayList();
			categories = new ArrayList();
			result.setSource(props.getAdapterCode());
			float relevance = props.getRelevanceMultiplier() * result.getRelevance();
			result.setRelevance((relevance > 1.0F) ? 1.0F : relevance);
			result.setHitFrequency((relevance > 1.0F) ? 1.0F : relevance);
			// derive any values?
			String derivation = PropertiesService.getStringProperty(props.getAdapterCode() + ".metadata.crosswalk.link.derivation", "");
			if (derivation.length() > 0) {
				result.setLink(StrSubstitutor.replace(derivation, docFieldValues));
			}
			results.add(result);
			result = new SearchResult();
		} else if (readingField && (localName.equals("int") || localName.equals("str") || localName.equals("long") || localName.equals("float") || localName.equals("double") || localName.equals("boolean") || localName.equals("date"))) {
			resultMetadata = new ResultMetadata(fieldName, fieldData.toString());
			metadata.add(resultMetadata);
			extractResultFields(fieldName, fieldData.toString());
			fieldData = new StringBuffer();
			if (!readingArray) {
				readingField = false;
			}
		} else if (readingField && readingArray && localName.equals("arr")) {
			readingField = false;
		} else if (readingParam && (localName.equals("int") || localName.equals("str") || localName.equals("long") || localName.equals("float") || localName.equals("double") || localName.equals("boolean") || localName.equals("date"))) {
			if ("numFound".equals(paramName)) {
				try {
					found = Integer.parseInt(paramData.toString());
				} catch (NumberFormatException nfe) {
				}
			} else if ("start".equals(paramName)) {
				try {
					start = Integer.parseInt(paramData.toString());
				} catch (NumberFormatException nfe) {
				}
			}
			paramData = new StringBuffer();
			readingParam = false;
		} else if ((readingFacetQueries || readingFacetFields) && localName.equals("int")) {
			// collect facet query count, store against query
			facetQueryCounts.put(facetQuery, new Integer(facetQueryCount));
		}

	}

	/**
	 * Gets, title, description, etc from named fields. Uses crosswalks defined
	 * in adapters properties.
	 */
	private void extractResultFields(String key, String value) {
		docFieldValues.put(key, value);
		if (PropertiesService.getStringProperty(props.getAdapterCode() + ".metadata.crosswalk.category", "").equalsIgnoreCase(key)) {
			ResultCategory cat = new ResultCategory();
			cat.setName(value);
			categories.add(cat);
		}
		if (PropertiesService.getStringProperty(props.getAdapterCode() + ".metadata.crosswalk.collectionName", "").equalsIgnoreCase(key)) {
			result.setCollectionName(value);
		}
		if (PropertiesService.getStringProperty(props.getAdapterCode() + ".metadata.crosswalk.contentPackage", "").equalsIgnoreCase(key)) {
			result.setContentPackage(value);
		}
		if (PropertiesService.getStringProperty(props.getAdapterCode() + ".metadata.crosswalk.date", "").equalsIgnoreCase(key)) {
			String dateFormatPattern = PropertiesService.getStringProperty(props.getAdapterCode() + ".metadata.crosswalk.date.format", null);
			SimpleDateFormat format = (dateFormatPattern == null) ? new SimpleDateFormat() : new SimpleDateFormat(dateFormatPattern);
			try {
				result.setDate(format.parse(value));
			} catch (ParseException se) {
			}
		}
		if (PropertiesService.getStringProperty(props.getAdapterCode() + ".metadata.crosswalk.description", "").equalsIgnoreCase(key)) {
			if (result.getDescription() != null && result.getDescription().length() > 0 && PropertiesService.getStringProperty(props.getAdapterCode() + ".metadata.crosswalk.description.multihandling", "").equalsIgnoreCase("concat")) {
				result.setDescription(result.getDescription() + " " + value);
			} else {
				result.setDescription(value);
			}
		}
		if (PropertiesService.getStringProperty(props.getAdapterCode() + ".metadata.crosswalk.download", "").equalsIgnoreCase(key)) {
			result.setDownload(value);
		}
		if (PropertiesService.getStringProperty(props.getAdapterCode() + ".metadata.crosswalk.hitFrequency", "").equalsIgnoreCase(key)) {
			result.setHitFrequency(Float.parseFloat(value));
		}
		if (PropertiesService.getStringProperty(props.getAdapterCode() + ".metadata.crosswalk.identifier", "").equalsIgnoreCase(key)) {
			result.setIdentifier(value);
		}
		if (PropertiesService.getStringProperty(props.getAdapterCode() + ".metadata.crosswalk.link", "").equalsIgnoreCase(key)) {
			result.setLink(value);
		}
		if (PropertiesService.getStringProperty(props.getAdapterCode() + ".metadata.crosswalk.metadataScheme", "").equalsIgnoreCase(key)) {
			result.setMetadataScheme(value);
		}
		if (PropertiesService.getStringProperty(props.getAdapterCode() + ".metadata.crosswalk.preview", "").equalsIgnoreCase(key)) {
			result.setPreview(value);
		}
		if (PropertiesService.getStringProperty(props.getAdapterCode() + ".metadata.crosswalk.relevance", "").equalsIgnoreCase(key)) {
			result.setRelevance(Float.parseFloat(value));
		}
		if (PropertiesService.getStringProperty(props.getAdapterCode() + ".metadata.crosswalk.rights", "").equalsIgnoreCase(key)) {
			result.setRights(value);
		}
		if (PropertiesService.getStringProperty(props.getAdapterCode() + ".metadata.crosswalk.source", "").equalsIgnoreCase(key)) {
			result.setSource(value);
		}

		if (PropertiesService.getStringProperty(props.getAdapterCode() + ".metadata.crosswalk.title", "").equalsIgnoreCase(key)) {

			result.setTitle(value);
		}

		String metadataFieldName = (String) solrToMetadataFieldMap.get(key);
		if (metadataFieldName != null && metadataFieldName.length() > 0) {
			metadata.add(new ResultMetadata(metadataFieldName, translateMetadataFieldValueForVocab(metadataFieldName, value)));
		}
	}

	private String translateMetadataFieldValueForVocab(String fieldName, String value) {
		if (PropertiesService.getBooleanProperty(props.getAdapterCode() + ".metadata.crosswalk.metadata." + fieldName + ".usevocab", false)) {
			String translated = VocabMapper.getKeyForValue(props.getAdapterCode(), fieldName, value);
			if (translated != null)
				return translated;
		}
		return value;
	}

	/**
	 * @param target
	 * @param data
	 * @exception SAXException
	 */
	public void processingInstruction(String target, String data) throws SAXException {
	}

	private ResultMetadata[] getAsMetadataArray(List list) {
		ResultMetadata[] array = new ResultMetadata[list.size()];
		int x = 0;
		for (Iterator i = list.iterator(); i.hasNext(); x++) {
			array[x] = (ResultMetadata) i.next();
		}
		return array;
	}

	private ResultCategory[] getAsCategoryArray(List list) {
		ResultCategory[] array = new ResultCategory[list.size()];
		int x = 0;
		for (Iterator i = list.iterator(); i.hasNext(); x++) {
			array[x] = (ResultCategory) i.next();
		}
		return array;
	}

	public Map getFacetQueryCounts() {
		return facetQueryCounts;
	}

	public void setFacetQueryCounts(Map facetQueryCounts) {
		this.facetQueryCounts = facetQueryCounts;
	}

}
